/*
   Copyright 2009 Abdulla Abdurakhmanov (abdulla@latestbit.com)
   Original sources are available at www.latestbit.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package org.latestbit.smartobjects.core;

import java.io.IOException;

import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeoutException;

import org.latestbit.smartobjects.*;
import org.latestbit.smartobjects.coders.*;
import org.latestbit.smartobjects.exceptions.*;
import org.latestbit.smartobjects.io.*;
import org.latestbit.smartobjects.network.*;
import org.latestbit.smartobjects.utils.*;


public abstract class RemoteSmartObject extends SmartObject implements IRemoteSmartObject, ICommandProcessorHandler, IManagedConnectionListener {	

	public static final int COMMAND_BUF_INITIAL_CAPACITY = 1024;

	public static final int COMMAND_BUF_MAX_CAPACITY = 1024 * 1024 * 32; // 32 Mb

	public static final int COMMAND_BUF_STEP_INCREMENT_CAPACITY = 1024;	
	
	private RequestContext defaultRequestContext = new RequestContext();
	
	protected Queue<ResizableByteBuffer> buffersPool = new LinkedList<ResizableByteBuffer>(); 

	private IManagedConnection managedConnection;
	
	protected List<IRemoteSmartObjectAvailabilityListener<? extends IRemoteSmartObject>> availabilityListeners = new LinkedList< IRemoteSmartObjectAvailabilityListener<? extends IRemoteSmartObject> > ();
	protected PostActionListenersContainer<IRemoteSmartObjectAvailabilityListener<? extends IRemoteSmartObject>> palAvailabilityListeners = new PostActionListenersContainer<IRemoteSmartObjectAvailabilityListener<? extends IRemoteSmartObject>> ();

	private boolean availableStatus;
	
	
	protected RemoteSmartObject () {
		this( null );
	}

	protected RemoteSmartObject ( String instanceId ) {
		super( instanceId );
		
	}
	
	@Override
	public synchronized void resolve ( ISmartBus bus, IEndpointLocation location ) throws SmartBusSystemException, InterruptedException {
		try {
			if(this.busServer!=null || this.busClient!=null) {
				destroy();
			}
			setBus(bus);
			this.busClient = bus.selectBusClient(location);
			this.availableStatus = false;
			this.managedConnection = this.busClient.getManagedClient(); 
			this.managedConnection.addConnectionListener(this);
			this.busClient.start();
		}
		catch(IOException ex) {
			throw new SmartBusSystemException ( ex );
		}
	}
	
	@Override
	public synchronized void resolve ( IRemoteSmartObject baseClient ) throws SmartBusSystemException {
		if(this.busServer!=null || this.busClient!=null) {
			destroy();
		}
		setBus(baseClient.getBus());
		this.busClient = baseClient.getBusClient();// busClient;
		this.busClient.addRef();
		this.managedConnection = this.busClient.getManagedClient(); 		
		this.busClient.getManagedClient().addConnectionListener(this);
		this.availableStatus = baseClient.isAvailable();
	}

	@Override
	public synchronized void resolve ( ISmartObject baseObject, ConnectionHandler connectionHandler ) throws SmartBusSystemException {
		if(baseObject.getBusServer() == null && baseObject.getBusClient() == null)
			throw new SmartBusSystemException("Unable to resolve object on unpublished/resolved base object");
		
		if(this.busServer!=null || this.busClient!=null) {
			destroy();
		}

		setBus(baseObject.getBus());
		if(baseObject.getBusServer()!=null) {
			this.busServer = baseObject.getBusServer();
			this.busServer.addRef();
		}
		else {
			this.busClient = baseObject.getBusClient();
			this.busClient.addRef();
		}
		this.availableStatus = true;
		this.managedConnection = new ProxyManagedConnection( connectionHandler );
		this.managedConnection.addConnectionListener(this);
	}	
	
	@Override
	public synchronized void destroy() {
		if(this.managedConnection != null ) {
			this.managedConnection.delConnectionListener(this);
			this.managedConnection = null;
		}
		if(this.busClient!=null) {
			getBus().returnBusClient(this.busClient);
			busClient = null;
		}
		else
		if(this.busServer!=null) {
			getBus().returnBusServer(this.busServer);
			this.busServer = null;
		}
	}	
	
	@Override
	public void finalize() {
		destroy();
	}	 
	
	@Override
	public void setDefaultRequestContext(RequestContext defaultRequestContext) {
		if(defaultRequestContext == null)
			throw new IllegalArgumentException("Default request context can't be null");
		this.defaultRequestContext = defaultRequestContext;
	}

	@Override
	public RequestContext getDefaultRequestContext() {
		return defaultRequestContext;
	}
	
	private int getTimeoutInSec(CallInterfaceOperationRequest request) {
		int timeoutInSec = RequestContext.DEFAULT_COMMAND_OPERATION_TIMEOUT;
		if ( request.getRequestContext() != null )
			timeoutInSec = request.getRequestContext().getMaxResponseTimeoutInSec();
		else
			if ( this.getDefaultRequestContext() != null )
				timeoutInSec = this.getDefaultRequestContext().getMaxResponseTimeoutInSec();
		return timeoutInSec;
	}
		
	public CallInterfaceOperationRequest createCallRequest(String operationId, RequestContext context) throws SmartBusSystemException {
		if(getBus() == null)
			throw new SmartBusSystemException("Unable to call method for unresolved remote object!");
		CallInterfaceOperationRequest request = null;
				
		//if(request==null) {
			ResizableByteBuffer buffer = null;
			synchronized(buffersPool) {
				if(buffersPool.isEmpty()) {
					buffer = new ResizableByteBuffer(
							false, 
							RemoteSmartObject.COMMAND_BUF_INITIAL_CAPACITY, 
							RemoteSmartObject.COMMAND_BUF_MAX_CAPACITY, 
							RemoteSmartObject.COMMAND_BUF_STEP_INCREMENT_CAPACITY
					);
				}
				else {
					buffer = buffersPool.poll();
					buffer.getBuffer().clear();
				}
			}
			
			request = new CallInterfaceOperationRequest(
					buffer, getBus().createDataTypesCoder()
			);
		//}
		request.setDestination(this.getInterfaceId());
		request.setOperationId(operationId);
		request.setMessageId( (int) this.getBus().getSequenceGenerator().next() );
		request.setInstanceId(getInstanceId());
		request.setRequestContext( context==null ? this.getDefaultRequestContext() : context );
		request.beginSerialize();
		return request;
	}
	
	public ConnectionHandler getConnectionHandlerToSend() throws SmartBusSystemException {
		if(this.managedConnection== null)
			throw new SmartBusSystemException("Unable to call method for unresolved remote object!");
		try {
			return this.managedConnection.getConnectionHandlerToSend();
		}
		catch(IOException ex) {
			throw new SmartBusSystemException ( ex );
		}		
	}
	
	public void callAsync(ConnectionHandler connectionHandler, CallInterfaceOperationRequest request, Object userObject) throws SmartBusSystemException {		
		request.finishSerialize();
		try {
			// sendAsyncRequest (connectionHandler, request, this, userObject, getTimeoutInSec(request) );
			ICommandProcessor commandProcessor = getCurrentCommandProcessor();
			if(commandProcessor == null)
				throw new SmartBusSystemException("Unable to call method for unresolved remote object!");
			commandProcessor.sendAsyncRequest(connectionHandler, request, this, userObject,getTimeoutInSec(request) );
		}
		catch(IOException ex) {
			throw new SmartBusSystemException ( ex );
		}
	}

	public CallInterfaceOperationResponse call(ConnectionHandler connectionHandler, CallInterfaceOperationRequest request)
			throws SmartBusSystemException, TimeoutException, InterruptedException {	
		request.finishSerialize();
		try {
			ICommandProcessor commandProcessor = getCurrentCommandProcessor();
			if(commandProcessor == null)
				throw new SmartBusSystemException("Unable to call method for unresolved remote object!");
			
			INetworkCommand command = // sendSyncRequest( connectionHandler, request, getTimeoutInSec(request) );
				commandProcessor.sendSyncRequest(connectionHandler, request, getTimeoutInSec(request));
			CallInterfaceOperationResponse response = new CallInterfaceOperationResponse(command);
			response.deserialize();
			return response;
		}
		catch(IOException ex) {
			throw new SmartBusSystemException( ex );
		}
	}
	
	protected ICommandProcessor getCurrentCommandProcessor() {
		ICommandProcessor commandProcessor = null;
		if(busServer!=null)
			commandProcessor = busServer.getCommandProcessor();
		else
		if(busClient!=null)
			commandProcessor = busClient.getCommandProcessor();
		return commandProcessor;
	}
	
	@Override
	public void onResponseReceived(ConnectionHandler handler, INetworkCommand request, INetworkCommand response, Object userObject) {
		updateLastActivity();
		CallInterfaceOperationResponse callResponse = new CallInterfaceOperationResponse(response);
		try {
			callResponse.deserialize();
			onCallResponseReceived(handler, request, callResponse, userObject);
			returnBufferToPool(response);
			
		} catch (SmartBusSystemException e) {
			System.err.println( request.getBuffer().getBuffer() );
			e.printStackTrace();
		}
	}

	protected void returnBufferToPool(INetworkCommand response) {
		synchronized(buffersPool) {
			buffersPool.add( response.getBuffer() );
			response.setBuffer(null);
		}		
	}
	
	protected abstract void onCallResponseReceived (
			ConnectionHandler handler,
			INetworkCommand request,
			CallInterfaceOperationResponse callResponse, Object userObject);

	@Override
	public abstract void onResponseTimeout(INetworkCommand request, Object userObject);
	
	@Override
	public void onRequestReceived(ConnectionHandler handler, INetworkCommand request) {
		// Nothing to do
	}
	
	@Override
	public <T extends IRemoteSmartObject> void addAvailabilityListener(IRemoteSmartObjectAvailabilityListener<T> listener) {
		synchronized(availabilityListeners) {
			if(!this.palAvailabilityListeners.isActivated()) {
				availabilityListeners.add(listener);
			}
			else
				this.palAvailabilityListeners.putListenerToAdd(listener);
		}
	}
	
	@Override
	public <T extends IRemoteSmartObject> void delAvailabilityListener(IRemoteSmartObjectAvailabilityListener<T> listener) {
		synchronized(availabilityListeners) {
			if(!this.palAvailabilityListeners.isActivated()) {
				availabilityListeners.remove(listener);
			}
			else
				this.palAvailabilityListeners.putListenerToRemove(listener);
		}		
	}
	
	@Override
	public void onConnected(IManagedConnection mconnection) {
		fireOnAvailable();
	}
	
	@Override
	public void onConnected(ConnectionHandler handler) {};
		
	@Override
	public void onDisconnected(IManagedConnection mconnection) {
		fireOnUnavailable();
	}

	@Override
	public boolean isAvailable() {
		synchronized(availabilityListeners) {
			return this.availableStatus;
		}
	}
	
	@Override
	public void onDisconnected(ConnectionHandler handler) {};	

	@Override
	public void onConnectionFailure(IManagedConnection mconnection, String errorMessage ) {};
	
	@Override
	public void onConnectionFailure(ConnectionHandler handler, String errorMessage) {};

	@Override
	public boolean isCommandIdSupported(ConnectionHandler handler, INetworkCommand request) {
		return false; // Any request command not supported
	}

	@SuppressWarnings( {"unchecked", "rawtypes" })
	protected void fireOnAvailable() {
		synchronized(availabilityListeners) {
			this.availableStatus = true;
			palAvailabilityListeners.activate();
			try {
				for(IRemoteSmartObjectAvailabilityListener<? extends IRemoteSmartObject> listener : availabilityListeners) {
					((IRemoteSmartObjectAvailabilityListener)listener).onAvailable(this);
				}
			}
			finally {
				processingPostActionAvailabilityListeners();
			}
		}
	}

	@SuppressWarnings( {"unchecked", "rawtypes" })
	protected void fireOnUnavailable() {
		synchronized(availabilityListeners) {
			this.availableStatus = false;			
			palAvailabilityListeners.activate();
			try {			
				for(IRemoteSmartObjectAvailabilityListener<? extends IRemoteSmartObject> listener : availabilityListeners) {
					((IRemoteSmartObjectAvailabilityListener)listener).onUnavailable(this);
				}
			}
			finally {
				processingPostActionAvailabilityListeners();
			}				
		}
	}
		
	
	protected void processingPostActionAvailabilityListeners() {
		palAvailabilityListeners.deactivate();
		try {
			for ( PostActionListenersContainer<IRemoteSmartObjectAvailabilityListener<? extends IRemoteSmartObject>>.PostActionQueueItem item : palAvailabilityListeners.getPostActionQueue() ) {
				if( item.isActionAdd() ) {
					addAvailabilityListener( item.getListener() );
				}
				else
				if( item.isActionRemove() ) {
					delAvailabilityListener( item.getListener() );
				}
			}
		}
		finally {
			palAvailabilityListeners.reset();
		}	
	}	
}
